import { intersection } from "@web/core/utils/arrays";
import { _t, appTranslateFn } from "@web/core/l10n/translation";
import { renderToElement } from "@web/core/utils/render";
import { App, Component } from "@odoo/owl";
import { getTemplate } from "@web/core/templates";
import { UrlAutoComplete } from "@website/components/autocomplete_with_pages/url_autocomplete";
import * as urlUtils from "@html_editor/utils/url";
import { patch } from "@web/core/utils/patch";

/**
 * Allows to load anchors from a page.
 *
 * @param {string} url
 * @param {Node} body the editable for which to recover anchors
 * @returns {Deferred<string[]>}
 */
function loadAnchors(url, body) {
    return new Promise(function (resolve, reject) {
        if (url === window.location.pathname || url[0] === "#") {
            resolve(body ? body.outerHTML : document.body.outerHTML);
        } else if (url.length && !url.startsWith("http")) {
            // TODO: Might be broken with ReplaceMedia (NBY) and LinkTools
            fetch(window.location.origin + url)
                .then((response) => response.text())
                .then((text) => {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(text, "text/html");
                    return doc.body;
                })
                .then(resolve, reject);
        } else {
            // avoid useless query
            resolve();
        }
    })
        .then(function (response) {
            const fragment = new DOMParser().parseFromString(response, "text/html");
            const anchorEls = fragment.querySelectorAll(
                `[id][data-anchor="true"], .modal[id][data-display="onClick"]`
            );
            const anchors = Array.from(anchorEls).map((el) => "#" + el.id);

            // Always suggest the top and the bottom of the page as internal link
            // anchor even if the header and the footer are not in the DOM. Indeed,
            // the "scrollTo" function handles the scroll towards those elements
            // even when they are not in the DOM.
            if (!anchors.includes("#top")) {
                anchors.unshift("#top");
            }
            if (!anchors.includes("#bottom")) {
                anchors.push("#bottom");
            }
            return anchors;
        })
        .catch((error) => {
            console.debug(error);
            return [];
        });
}

/**
 * Allows the given input to propose existing website URLs.
 *
 * @param {HTMLInputElement} input
 */
function autocompleteWithPages(input, options = {}, env = undefined) {
    const owlApp = new App(UrlAutoComplete, {
        env: env || Component.env,
        dev: env ? env.debug : Component.env.debug,
        getTemplate,
        props: {
            options,
            loadAnchors,
            targetDropdown: input,
        },
        translatableAttributes: ["data-tooltip"],
        translateFn: appTranslateFn,
    });

    const container = document.createElement("div");
    container.classList.add("ui-widget", "ui-autocomplete", "ui-widget-content", "border-0");
    document.body.appendChild(container);
    owlApp.mount(container);
    return () => {
        owlApp.destroy();
        container.remove();
    };
}

/**
 * @param {jQuery} $element
 * @param {jQuery} [$excluded]
 */
function onceAllImagesLoaded($element, $excluded) {
    var defs = Array.from($element.find("img").addBack("img")).map((img) => {
        if (img.complete || ($excluded && ($excluded.is(img) || $excluded.has(img).length))) {
            return; // Already loaded
        }
        var def = new Promise(function (resolve, reject) {
            $(img).one("load", function () {
                resolve();
            });
        });
        return def;
    });
    return Promise.all(defs);
}

/**
 * @deprecated
 * @todo create Dialog.prompt instead of this
 */
function prompt(options, _qweb) {
    /**
     * A bootstrapped version of prompt() albeit asynchronous
     * This was built to quickly prompt the user with a single field.
     * For anything more complex, please use editor.Dialog class
     *
     * Usage Ex:
     *
     * website.prompt("What... is your quest?").then(function (answer) {
     *     arthur.reply(answer || "To seek the Holy Grail.");
     * });
     *
     * website.prompt({
     *     select: "Please choose your destiny",
     *     init: function () {
     *         return [ [0, "Sub-Zero"], [1, "Robo-Ky"] ];
     *     }
     * }).then(function (answer) {
     *     mame_station.loadCharacter(answer);
     * });
     *
     * @param {Object|String} options A set of options used to configure the prompt or the text field name if string
     * @param {String} [options.window_title=''] title of the prompt modal
     * @param {String} [options.input] tell the modal to use an input text field, the given value will be the field title
     * @param {String} [options.textarea] tell the modal to use a textarea field, the given value will be the field title
     * @param {String} [options.select] tell the modal to use a select box, the given value will be the field title
     * @param {Object} [options.default=''] default value of the field
     * @param {Function} [options.init] optional function that takes the `field` (enhanced with a fillWith() method) and the `dialog` as parameters [can return a promise]
     */
    if (typeof options === "string") {
        options = {
            text: options,
        };
    }
    if (typeof _qweb === "undefined") {
        _qweb = "website.prompt";
    }
    options = Object.assign(
        {
            window_title: "",
            field_name: "",
            default: "", // dict notation for IE<9
            init: function () {},
            btn_primary_title: _t("Create"),
            btn_secondary_title: _t("Cancel"),
        },
        options || {}
    );

    var type = intersection(Object.keys(options), ["input", "textarea", "select"]);
    type = type.length ? type[0] : "input";
    options.field_type = type;
    options.field_name = options.field_name || options[type];

    var def = new Promise(function (resolve, reject) {
        var dialog = $(renderToElement(_qweb, options)).appendTo("body");
        options.$dialog = dialog;
        var field = dialog.find(options.field_type).first();
        field.val(options["default"]); // dict notation for IE<9
        field.fillWith = function (data) {
            if (field.is("select")) {
                var select = field[0];
                data.forEach(function (item) {
                    select.options[select.options.length] = new window.Option(item[1], item[0]);
                });
            } else {
                field.val(data);
            }
        };
        var init = options.init(field, dialog);
        Promise.resolve(init).then(function (fill) {
            if (fill) {
                field.fillWith(fill);
            }
            dialog.modal("show");
            field.focus();
            dialog.on("click", ".btn-primary", function () {
                var backdrop = $(".modal-backdrop");
                resolve({ val: field.val(), field: field, dialog: dialog });
                dialog.modal("hide").remove();
                backdrop.remove();
            });
        });
        dialog.on("hidden.bs.modal", function () {
            var backdrop = $(".modal-backdrop");
            reject();
            dialog.remove();
            backdrop.remove();
        });
        if (field.is('input[type="text"], select')) {
            field.keypress(function (e) {
                if (e.key === "Enter") {
                    e.preventDefault();
                    dialog.find(".btn-primary").trigger("click");
                }
            });
        }
    });

    return def;
}

function websiteDomain(self) {
    var websiteID;
    self.trigger_up("context_get", {
        callback: function (ctx) {
            websiteID = ctx["website_id"];
        },
    });
    return ["|", ["website_id", "=", false], ["website_id", "=", websiteID]];
}

/**
 * Checks if the 2 given URLs are the same, to prevent redirecting uselessly
 * from one to another.
 * It will consider naked URL and `www` URL as the same URL.
 * It will consider `https` URL `http` URL as the same URL.
 *
 * @param {string} url1
 * @param {string} url2
 * @returns {Boolean}
 */
function isHTTPSorNakedDomainRedirection(url1, url2) {
    try {
        url1 = new URL(url1).host;
        url2 = new URL(url2).host;
    } catch {
        // Incorrect URL, `false` URL..
        return false;
    }
    return url1 === url2 || url1.replace(/^www\./, "") === url2.replace(/^www\./, "");
}

export function sendRequest(route, params) {
    function _addInput(form, name, value) {
        const param = document.createElement("input");
        param.setAttribute("type", "hidden");
        param.setAttribute("name", name);
        param.setAttribute("value", value);
        form.appendChild(param);
    }

    const form = document.createElement("form");
    form.setAttribute("action", route);
    form.setAttribute("method", params.method || "POST");
    // This is an exception for the 404 page create page button, in backend we
    // want to open the response in the top window not in the iframe.
    if (params.forceTopWindow) {
        form.setAttribute("target", "_top");
    }

    if (odoo.csrf_token) {
        _addInput(form, "csrf_token", odoo.csrf_token);
    }

    for (const key in params) {
        const value = params[key];
        if (Array.isArray(value) && value.length) {
            for (const val of value) {
                _addInput(form, key, val);
            }
        } else {
            _addInput(form, key, value);
        }
    }

    document.body.appendChild(form);
    form.submit();
}

/**
 * Converts a base64 SVG into a base64 PNG.
 *
 * @param {string|HTMLImageElement} src - an URL to a SVG or a *loaded* image
 *      with such an URL. This allows the call to potentially be a bit more
 *      efficient in that second case.
 * @returns {Promise<string>} a base64 PNG (as result of a Promise)
 */
export async function svgToPNG(src) {
    return _exportToPNG(src, "svg+xml");
}

/**
 * Converts a base64 WEBP into a base64 PNG.
 *
 * @param {string|HTMLImageElement} src - an URL to a WEBP or a *loaded* image
 *     with such an URL. This allows the call to potentially be a bit more
 *     efficient in that second case.
 * @returns {Promise<string>} a base64 PNG (as result of a Promise)
 */
export async function webpToPNG(src) {
    return _exportToPNG(src, "webp");
}

/**
 * Converts a formatted base64 image into a base64 PNG.
 *
 * @private
 * @param {string|HTMLImageElement} src - an URL to a image or a *loaded* image
 *     with such an URL. This allows the call to potentially be a bit more
 *     efficient in that second case.
 * @param {string} format - the format of the image
 * @returns {Promise<string>} a base64 PNG (as result of a Promise)
 */
async function _exportToPNG(src, format) {
    function checkImg(imgEl) {
        // Firefox does not support drawing SVG to canvas unless it has width
        // and height attributes set on the root <svg>.
        return imgEl.naturalHeight !== 0;
    }
    function toPNGViaCanvas(imgEl) {
        const canvas = document.createElement("canvas");
        canvas.width = imgEl.width;
        canvas.height = imgEl.height;
        canvas.getContext("2d").drawImage(imgEl, 0, 0);
        return canvas.toDataURL("image/png");
    }

    // In case we receive a loaded image and that this image is not problematic,
    // we can convert it to PNG directly.
    if (src instanceof HTMLImageElement) {
        const loadedImgEl = src;
        if (checkImg(loadedImgEl)) {
            return toPNGViaCanvas(loadedImgEl);
        }
        src = loadedImgEl.src;
    }

    // At this point, we either did not receive a loaded image or the received
    // loaded image is problematic => we have to do some asynchronous code.
    return new Promise((resolve) => {
        const imgEl = new Image();
        imgEl.onload = () => {
            if (format !== "svg+xml" || checkImg(imgEl)) {
                resolve(imgEl);
                return;
            }

            // Set arbitrary height on image and attach it to the DOM to force
            // width computation.
            imgEl.height = 1000;
            imgEl.style.opacity = 0;
            document.body.appendChild(imgEl);

            const request = new XMLHttpRequest();
            request.open("GET", imgEl.src, true);
            request.onload = () => {
                // Convert the data URI to a SVG element
                const parser = new DOMParser();
                const result = parser.parseFromString(request.responseText, "text/xml");
                const svgEl = result.getElementsByTagName("svg")[0];

                // Add the attributes Firefox needs and remove the image from
                // the DOM.
                svgEl.setAttribute("width", imgEl.width);
                svgEl.setAttribute("height", imgEl.height);
                imgEl.remove();

                // Convert the SVG element to a data URI
                const svg64 = btoa(new XMLSerializer().serializeToString(svgEl));
                const finalImg = new Image();
                finalImg.onload = () => {
                    resolve(finalImg);
                };
                finalImg.src = `data:image/svg+xml;base64,${svg64}`;
            };
            request.send();
        };
        imgEl.src = src;
    }).then((loadedImgEl) => toPNGViaCanvas(loadedImgEl));
}

/**
 * Bootstraps an "empty" Google Maps iframe.
 *
 * @returns {HTMLIframeElement}
 */
export function generateGMapIframe() {
    const iframeEl = document.createElement("iframe");
    iframeEl.classList.add("s_map_embedded", "o_not_editable");
    iframeEl.setAttribute("width", "100%");
    iframeEl.setAttribute("height", "100%");
    iframeEl.setAttribute("frameborder", "0");
    iframeEl.setAttribute("scrolling", "no");
    iframeEl.setAttribute("marginheight", "0");
    iframeEl.setAttribute("marginwidth", "0");
    iframeEl.setAttribute("src", "about:blank");
    iframeEl.setAttribute("aria-label", _t("Map"));
    return iframeEl;
}

/**
 * Generates a Google Maps URL based on the given parameter.
 *
 * @param {DOMStringMap} dataset
 * @returns {string} a Google Maps URL
 */
export function generateGMapLink(dataset) {
    return (
        "https://maps.google.com/maps?q=" +
        encodeURIComponent(dataset.mapAddress) +
        "&t=" +
        encodeURIComponent(dataset.mapType) +
        "&z=" +
        encodeURIComponent(dataset.mapZoom) +
        "&ie=UTF8&iwloc=&output=embed"
    );
}

/**
 * Checks if the edited content is currently previewed as in a mobile device.
 *
 * @param {Object} self - context object ("this")
 * @returns {boolean}
 */
function isMobile(self) {
    let isMobile;
    self.trigger_up("service_context_get", {
        callback: (ctx) => {
            isMobile = ctx["isMobile"];
        },
    });

    return isMobile;
}

/**
 * Returns the parsed data coming from the data-for element for the given form.
 *
 * @param {string} formId
 * @param {HTMLElement} parentEl
 * @returns {Object|undefined} the parsed data
 */
function getParsedDataFor(formId, parentEl) {
    const dataForEl = parentEl.querySelector(`[data-for='${formId}']`);
    if (!dataForEl) {
        return;
    }
    return JSON.parse(
        dataForEl.dataset.values
            // replaces `True` by `true` if they are after `,` or `:` or `[`
            .replace(/([,:[]\s*)True/g, "$1true")
            // replaces `False` and `None` by `""` if they are after `,` or `:` or `[`
            .replace(/([,:[]\s*)(False|None)/g, '$1""')
            // replaces the `'` by `"` if they are before `,` or `:` or `]` or `}`
            .replace(/'(\s*[,:\]}])/g, '"$1')
            // replaces the `'` by `"` if they are after `{` or `[` or `,` or `:`
            .replace(/([{[:,]\s*)'/g, '$1"')
    );
}

/**
 * Deep clones children or parses a string into elements, with or without
 * <script> elements.
 *
 * @param {DocumentFragment|HTMLElement|String} content
 * @param {Boolean} [keepScripts=false] - whether to keep script tags or not.
 * @returns {DocumentFragment}
 */
export function cloneContentEls(content, keepScripts = false) {
    let copyFragment;
    if (typeof content === "string") {
        copyFragment = new Range().createContextualFragment(content);
    } else {
        copyFragment = new DocumentFragment();
        const els = [...content.children].map((el) => el.cloneNode(true));
        copyFragment.append(...els);
    }
    if (!keepScripts) {
        copyFragment.querySelectorAll("script").forEach((scriptEl) => scriptEl.remove());
    }
    return copyFragment;
}

/**
 * Checks SEO data and notifies if either the page title or description is not
 * set.
 *
 * @param {Object} seo_data - The SEO data to check.
 * @param {Component} OptimizeSEODialog - Dialog to be displayed
 * @param {Object} services - Services object which will be used to display
 * notifications and dialog.
 */
export function checkAndNotifySEO(seo_data, OptimizeSEODialog, services) {
    if (seo_data) {
        let message;
        if (!seo_data.website_meta_title) {
            message = _t("Page title not set.");
        } else if (!seo_data.website_meta_description) {
            message = _t("Page description not set.");
        }
        if (message) {
            const closeNotification = services.notification.add(message, {
                type: "warning",
                sticky: false,
                buttons: [
                    {
                        name: _t("Optimize SEO"),
                        onClick: () => {
                            services.dialog.add(OptimizeSEODialog);
                            closeNotification();
                        },
                    },
                ],
            });
        }
    }
}

/**
 * Converts a string into a URL-friendly slug.
 *
 * @param {string} value - The string to slugify.
 * @returns {string} The slugified string.
 */
export function slugify(value) {
    // `NFKD` as in `http_routing` python `slugify()`
    return !value
        ? ""
        : value
              .trim()
              .normalize("NFKD")
              .toLowerCase()
              .replace(/['’]/g, "-") // Replace apostrophes with hyphens
              .replace(/\s+/g, "-") // Replace spaces with -
              .replace(/[^\w-]+/g, "") // Remove all non-word chars
              .replace(/--+/g, "-"); // Replace multiple - with single -
}

patch(urlUtils, {
    isAbsoluteURLInCurrentDomain(url, env = null) {
        const res = super.isAbsoluteURLInCurrentDomain(url, env);
        if (res) {
            return true;
        }

        const w = env?.services.website.currentWebsite;
        if (!w) {
            return false;
        }

        // Make sure that while being on abc.odoo.com, if you edit a link and
        // enter an absolute URL using your real domain, it is still considered
        // to be added as relative, preferably.
        // In the past, you could not edit your website from abc.odoo.com if you
        // properly configured your real domain already.
        let origin;
        try {
            // Needed: "http:" would crash
            origin = new URL(url, window.location.origin).origin;
        } catch {
            return false;
        }
        return `${origin}/`.startsWith(w.domain);
    },
});

export default {
    loadAnchors: loadAnchors,
    autocompleteWithPages: autocompleteWithPages,
    onceAllImagesLoaded: onceAllImagesLoaded,
    prompt: prompt,
    sendRequest: sendRequest,
    websiteDomain: websiteDomain,
    isHTTPSorNakedDomainRedirection: isHTTPSorNakedDomainRedirection,
    svgToPNG: svgToPNG,
    webpToPNG: webpToPNG,
    generateGMapIframe: generateGMapIframe,
    generateGMapLink: generateGMapLink,
    isMobile: isMobile,
    getParsedDataFor: getParsedDataFor,
    cloneContentEls: cloneContentEls,
    checkAndNotifySEO: checkAndNotifySEO,
    slugify: slugify,
};
